/**
 * *开发时间：2022/7/22
 * 该CPP源文件为集群聊天服务器的客户端代码文件
 */

//------宏定义-----
#define CONN_IP     "127.0.0.1"
#define CONN_PORT   6969

//C++标准库
#include <iostream>
#include <vector>
#include <string>
#include <thread>
#include <ctime>
#include <chrono>
#include <unordered_map>
#include <functional>
#include <atomic>   //原子类型

//Linux接口库
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <semaphore.h>

//第三方库
#include "../../thirdparty/json.hpp"

//自己开发的项目库文件
#include "group.hpp"
#include "user.hpp"
#include "public.hpp"

//命名空间引用相关
using std::cout;
using std::cin;
using std::endl;
using std::vector;
using std::string;
using std::unordered_map;

using nlohmann::json;

//-------全局函数声明--------

//显示当前登录成功的用户的基本信息（纯打印）
void showCurrentUserData();

//接受线程（防止单线程cin阻塞输入时，无法接收消息）
void readTaskHandler(int clientfd);

//获取系统时间（聊天时需要添加时间信息）
string getCurrentTime();

//聊天程序主界面
void mainMenu(int clientfd);


//下面是用户输入功能命令相关代码

#pragma region   功能函数声明

// 帮助提示函数，相当于控制台常见到的 xxx -help
void help(int, string);
// 聊天功能命令
void chat(int, string);
// 添加好友命令
void addfriend(int, string);
// 创建群组命令
void creategroup(int, string);
// 加入群组命令
void addgroup(int, string);
// 群组聊天命令
void groupchat(int, string);
// 退出账号命令
void loginout(int, string);

#pragma endregion 功能函数声明



//-------全局变量-------

//控制账号是否登录或注销（即是否继续在聊天界面循环）
bool isMainMenuRunning=false;

//记录登录状态的原子类型，因为是多线程访问，所以保证原子性
std::atomic_bool m_isLoginSuccess{false};

//存储该客户端所登录的用户的信息
User m_currentUser;

//存储该客户端所登录的用户的好友列表
vector<User> m_currentUserFriendList;

//记录当前用户的群组列表
vector<Group> m_currentUserGroupsList;

//用于读写线程之间通信的信号量
sem_t rwsem;



//各功能命令和他们的功能描述，格式描述
unordered_map<string, string> commandMap = {
    {"help", "显示所有支持的命令，格式help"},
    {"chat", "一对一聊天，格式chat:friendid:message"},
    {"addfriend", "添加好友，格式addfriend:friendid"},
    {"creategroup", "创建群组，格式creategroup:groupname:groupdesc"},
    {"addgroup", "加入群组，格式addgroup:groupid"},
    {"groupchat", "群聊，格式groupchat:groupid:message"},
    {"loginout", "注销，格式loginout"}};


//注册功能，各功能代码与函数指针绑定
//这里的function<void(int,string)>中
//int指的是传递clinentfd
//string指的是用户输入的命令行，如：chat:friendid:message
unordered_map<string, std::function<void(int, string)>> commandHandlerMap = {
    {"help", help},
    {"chat", chat},
    {"addfriend", addfriend},
    {"creategroup", creategroup},
    {"addgroup", addgroup},
    {"groupchat", groupchat},
    {"loginout", loginout}};


//聊天客户端实现，main函数作为发送json线程，子线程作接收线程
int main(int argc, char* argv[]){
    //客户端运行时需要附带 连接服务器ip 和 端口 的
    //如果没有的话（参数小于三个）就会报错
    if(argc<3){
        std::cerr<<"command invalid! example: ./ChatClient 127.0.0.1 6969 "<<endl;
        exit(-1);
    }

    //解析命令行参数argv
    char* ip=argv[1];
    uint16_t port=atoi(argv[2]);

    //创建客户端的套接字fd
    int clientfd=socket(AF_INET,SOCK_STREAM,0);
    if(clientfd==-1){
        //错误处理
        std::cerr<<"socket create error! "<<endl;
        exit(-1);
    }

    //网络通信地址
    sockaddr_in server;
    memset(&server,0,sizeof(server));

    //填写服务器的ip和端口信息
    server.sin_family=AF_INET;
    server.sin_port=htons(CONN_PORT);
    server.sin_addr.s_addr=inet_addr(CONN_IP);

    //TCP三次握手
    if(-1==connect(clientfd,(struct sockaddr*)&server,sizeof(server))){
        std::cerr<<"connect server error! "<<endl;
        exit(-1);
    }

    //初始化读写线程的信号量，第二个变量指是否在同一进程间共享
    sem_init(&rwsem,0,0);

    //connect成功后，启动负责读信息的子线程
    std::thread readTask(readTaskHandler,clientfd);
    readTask.detach();//分离线程
    
    //主循环，main函数线程负责发送数据
    while(true){
        //显示菜单：登录、注册、退出等
        cout<<"==============="<<endl;
        cout<<"\t1.Login"<<endl;
        cout<<"\t2.Register"<<endl;
        cout<<"\t3.Quit"<<endl;
        cout<<"==============="<<endl;
        cout<<"Please input your choice(1、2、3): ";

        int choice=0;
        cin>>choice;
        cin.get();//读掉缓冲区中残留的回车

        switch(choice){
            case 1: {     //登录操作
                int id=0;
                char password[50]={0};
                
                cout<<"UserID: ";
                cin>>id;
                cin.get();//读掉缓冲区中残留的回车
                cout<<"Passowrd: ";
                cin.getline(password,50);

                //组装登录用的Json
                json js;
                js["msgid"]=int(EnMsgType::LOG_MSG);
                js["id"]=id;
                js["password"]=password;
                string request=js.dump();

                //每次登录前都初始化原子变量
                m_isLoginSuccess=false;
                
                //发送登录Json到服务器
                int len=send(clientfd,request.c_str(),request.size()+1,0);
                if(len==-1){
                    //发送失败
                    std::cerr<<"send login message error: "<<request<<endl;
                    break;
                }else{
                    //发送成功，准备接收服务器返回过来的响应Json
                    //缓冲区大小大一点，因为涉及到群组信息，成员信息，好友信息，离线消息等
                    //char buffer[5120]={0};
                    //读取响应数据
                    //len=recv(clientfd,buffer,1024,0);

                    //发送成功后等待信号量，由子线程处理完后通知是否登录成功
                    sem_wait(&rwsem);

                    if(m_isLoginSuccess==true){
                        //登录成功
                        //允许进入聊天界面循环
                        isMainMenuRunning=true;
                        //显示主聊天菜单界面
                        mainMenu(clientfd);
                    }
                    
                    
                }//end send login

                break;

            }
            case 2:{    //注册操作
                char name[50]={0};
                char password[50]={0};

                cout<<"Username: ";
                cin.getline(name,50);
                cout<<"Password: ";
                cin.getline(password,50);

                //组装注册业务的json
                json js;
                js["msgid"]=int(EnMsgType::REG_MSG);//注册消息类型
                js["name"]=name;
                js["password"]=password;
                string request=js.dump();

                //客户端向服务端发送注册Json数据
                int len=send(clientfd,request.c_str(),request.size()+1,0);
                if(len==-1){
                    //发送注册数据失败
                    std::cerr<<"send register message error: "<<request<<endl;
                    break;
                }
                //主线程发送注册请求后，等待信号量
                sem_wait(&rwsem);
                break;
            }
            case 3:{//quit
                close(clientfd);

                //销毁信号量
                sem_destroy(&rwsem);
                exit(-1);
            }
            default:
                //其它情况的话就是用户输入错误了
                std::cerr<<"invalid input!"<<endl;
                choice=0;
                cin.get();
                //清空缓冲区
                cin.ignore(std::numeric_limits<std::streamsize>::max(),'\n');
                break;

        }//end switch

    }//end while(true)循环主体


}

//聊天页面的主体
void mainMenu(int clientfd){
    //进入主聊天页面之后，打印帮助命令
    help(0,"");

    //获取用户输入的命令行
    char buffer[1024]={0};
    while(isMainMenuRunning){
        //循环主体

        cin.getline(buffer,1024);
        
        //存储用户输入的命令行（未改动过的，即包含了:）
        string commandBuff(buffer);

        //存储用户输入的功能命令（比如：help,chat...），即裁剪了前面一段
        string command;

        //解析用户输入的命令行字符串，搜索 ':' 位置
        int idx=commandBuff.find(':');
        if(idx==string::npos){
            //找不到':'则直接将命令存储
            command=commandBuff;
        }else{
            //否则就要裁剪字符串，提取输入的指令是什么
            command=commandBuff.substr(0,idx);
        }
        //从功能关系表中查找是否command指令对应的功能函数
        auto it=commandHandlerMap.find(command);

        //若找不到，则提示用户需要重新输入
        if(it==commandHandlerMap.end()){
            std::cerr<<"invalid input command! "<<endl;
            //清空缓冲区
            cin.ignore(std::numeric_limits<std::streamsize>::max(),'\n');
            continue;
        }

        //调用function回调函数
        it->second(clientfd,commandBuff.substr(idx+1,commandBuff.size()-idx));


    }//end while
}



//显示登录之后，该用户的基本信息、好友列表和群组列表
void showCurrentUserData(){
    cout << "======================login user======================" << endl;
    cout << "current login user => id:" << m_currentUser.getId() << " name:" << m_currentUser.getName() << endl;
    cout << "----------------------friend list---------------------" << endl;
    if (!m_currentUserFriendList.empty())
    {
        for (User &user : m_currentUserFriendList)
        {
            cout << user.getId() << " " << user.getName() << " " << user.getState() << endl;
        }
    }
    cout << "----------------------group list----------------------" << endl;
    if (!m_currentUserGroupsList.empty())
    {
        for (Group &group : m_currentUserGroupsList)
        {
            cout << group.getID() << " " << group.getName() << " --- " << group.getDesc() << endl;
            cout<<"Menbers: "<<endl;
            for (GroupUser &user : group.getUsers())
            {
                cout << user.getId() << " " << user.getName() << " " << user.getState()
                     << " " << user.getRole() << endl;
            }
            cout<<"------------------------------------------------"<<endl;
        }
    }
    cout << "======================================================" << endl;
}




//处理登录的响应逻辑
void doLoginResponse(json& responseLogin){
    /**
     * 登录的errno有三种结果：
     * 0--登录成功，返回用户基本信息，离线消息以及好友列表等
     * 1--登录失败，id或密码错误
     * 2--登录失败，所登录的账号已在线
     */
    
    if(responseLogin["errno"].get<int>()!=0){
        //登录失败，直接输出响应json里面附带的错误信息
        std::cerr<<responseLogin["errmsg"]<<endl;
        //给原子量赋值为false
        m_isLoginSuccess=false;
        return;
    }else{
        //登录成功
        
        //记录当前用户信息
        m_currentUser.setID(responseLogin["id"].get<int>());
        m_currentUser.setName(responseLogin["name"]);

        //1、记录当前用户的好友列表
        //初始化全局数组
        m_currentUserFriendList.clear();

        //通过检测响应Json是否包含["friedns"]这个键来判断用户是否有好友
        if(responseLogin.contains("friends")){
            //包含好友列表则将值（也是个Json类型）拷贝到一个string数组中
            //并二次解析成每个User对象，将每个User对象打入到用于保存好友列表的全局数组中

            vector<string> jsFriendsArr=responseLogin["friends"];
            for(string& str:jsFriendsArr){
                json tempJson=json::parse(str);
                User user;
                user.setID(tempJson["id"].get<int>());
                user.setName(tempJson["name"]);
                user.setState(tempJson["state"]);
                
                //逐个将用户信息保存到全局数组中
                m_currentUserFriendList.push_back(user);
            }
        }

        //2、同样的，我们还要记录群组列表的信息

        //全局数组初始化
        m_currentUserGroupsList.clear();

        if(responseLogin.contains("groups")){
            vector<string> jsGroupsArr=responseLogin["groups"];
            for(string& str:jsGroupsArr){
                json tempJson=json::parse(str);
                Group group;
                group.setID(tempJson["groupid"].get<int>());
                group.setName(tempJson["groupname"]);
                group.setDesc(tempJson["groupdesc"]);

                //记录该群中成员列表的信息
                vector<string> usersInEachGroup=tempJson["users"];
                for(string& userStr: usersInEachGroup){
                    GroupUser user;
                    //将群组成员列表里面的用户逐个解析
                    json userInGroupJson=json::parse(userStr);

                    user.setID(userInGroupJson["id"].get<int>());
                    user.setName(userInGroupJson["name"]);
                    user.setState(userInGroupJson["state"]);
                    user.setRole(userInGroupJson["role"]);

                    //解析好后放入当前群组对象的成员列表数组中
                    group.getUsers().push_back(user);
                }
                //一个群组对象解析好后，将该群组对象放入记录群组的全局数组中
                m_currentUserGroupsList.push_back(group);
            }
        }
        //3、显示当前登录用户的基本信息
        showCurrentUserData();

        //4、显示离线消息
        if(responseLogin.contains("offlinemsg")){
            //有离线消息的话则直接输出
            vector<string> offlineMsgJsons=responseLogin["offlinemsg"];
            for(string& msgJsons: offlineMsgJsons){
                cout<<msgJsons<<endl;
                json curMsgJson=json::parse(msgJsons);
                //直接输出离线消息
                //在数据库中存储离线信息的内容是id对应聊天消息体json，json中即包括时间等信息
                //所以这里可以调用时间出来
                //格式为：time + [id] + name + " said: " + XXX
                cout<<curMsgJson["time"].get<string>()<<" ["<<curMsgJson["id"]<<"] "<<curMsgJson["name"]
                    <<" said: "<<curMsgJson["msg"].get<string>()<<endl;
            }
        }

        //登录成功给原子量赋值为true
        m_isLoginSuccess=true;
    }//end 登录成功后的操作
}

void doRegResponse(json& responseJson){

    //接收注册响应数据成功，准备解析响应Json
    if(responseJson["errno"].get<int>()!=0){
        //响应数据注册码表示注册失败
        std::cerr<<"register error, is already exist! "<<endl;

    }else{
        //注册成功，返回用户id，并提示用户要牢记（物理记住）
        std::cout<<"registered success, userid is: "<<responseJson["id"]<<" , do not forget it!"<<endl;
    }

}



//接收线程
void readTaskHandler(int clientfd){
    while(true){
        char buffer[4096]={0};//4kb大小的读缓冲区
        
        //recv读消息并获取长度返回值
        int len=recv(clientfd,buffer,4096,0);

        //读取错误或消息为空
        if(len==-1||len==0){
            std::cerr<<"readTaskHandler recv error! "<<endl;
            close(clientfd);
            exit(-1);
        }
        //若读取成功，解析json
        json js=json::parse(buffer);

        //分析不同类型的响应Json，从而做出不同的反应
        int msgid=js["msgid"].get<int>();
        EnMsgType msgType=EnMsgType(msgid);

        switch(msgType){
            case EnMsgType::ONE_CHAT_MSG:{
                //如果该消息是点对点聊天消息，则输出
                cout<<js["time"].get<string>()<<" ["<<js["id"]<<"] "<<js["name"].get<string>()
                    <<" said: "<<js["msg"].get<string>()<<endl;
                break;
            }

            //创建群组操作后的响应
            case EnMsgType::CREATE_GROUP_ACK:{
                int errid=js["errno"].get<int>();
                if(errid==0){
                    //创建群组成功，输出groupid
                    cout<<"create group success! groupid: "<<js["groupid"].get<int>()<<endl;
                }else{
                    //创建群组失败
                    cout<<js["errmsg"]<<endl;
                }
                break;
            }

            //加入群组功能响应
            case EnMsgType::ADD_GROUP_ACK:{
                int errid=js["errno"].get<int>();
                if(errid==0){
                    //加入群组成功，输出groupid
                    cout<<"user: "<<js["userid"].get<int>()<<" join group success! groupid: "<<js["groupid"].get<int>()<<endl;
                }else{
                    //加入群组失败
                    cout<<js["errmsg"]<<endl;
                }
                break;
            }

            //群组聊天消息
            case EnMsgType::GROUP_CHAT_MSG:{
                cout<<js["time"].get<string>()<<" Group MSG["<<js["groupid"].get<int>()<<"] - "<<js["name"].get<string>()<<"["<<js["id"]<<"] "
                    <<" said: "<<js["msg"].get<string>()<<endl;
                break;
            }

            //客户端登录
            case EnMsgType::LOG_MSG_ACK:{
                doLoginResponse(js);

                //不管登录成功与否都响应信号量
                //通知主线程，由主线程作判断
                sem_post(&rwsem);
                break;
            }

            //客户端注册响应
            case EnMsgType::REG_MSG_ACK:{
                doRegResponse(js);
                sem_post(&rwsem);
                break;
            }

            default:
                break;
        }
    }//end while
}


//获取系统时间（聊天时需要添加时间信息）
string getCurrentTime(){
    auto tt = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
    struct tm*ptm=localtime(&tt);
    char date[60]={0};
    sprintf(date,"%d-%02d-%02d %02d:%02d:%02d",
            (int)ptm->tm_year+1900,(int)ptm->tm_mon+1,(int)ptm->tm_mday,
            (int)ptm->tm_hour,(int)ptm->tm_min,(int)ptm->tm_sec);
    return std::string(date);
}




#pragma region 各功能函数的实现

// 帮助提示函数，相当于控制台常见到的 xxx -help
void help(int fd = 0, string str = ""){
    cout<<"show command list >>> \n";
    for(auto& comm:commandMap){
        cout<<comm.first<<" : "<<comm.second<<endl;
    }
    cout<<endl;
}

// 聊天功能命令 chat:friendid:message
void chat(int clientfd, string str){//此时str只剩下friendid:message
    
    int idx=str.find(':');
    if(idx==string::npos){
        std::cerr<<"chat command invalid! "<<endl;
        return;
    }
    //获取聊天好友（目标）id
    int friendid=atoi(str.substr(0,idx).c_str());

    //获取聊天消息正文内容
    string message=str.substr(idx+1,str.size()-idx);

    //封装发送信息的json
    json js;
    js["msgid"]=int(EnMsgType::ONE_CHAT_MSG);
    js["id"]=m_currentUser.getId();
    js["name"]=m_currentUser.getName();
    js["toid"]=friendid;
    js["msg"]=message;
    js["time"]=getCurrentTime();

    string buffer(js.dump());
    int len=send(clientfd,buffer.c_str(),buffer.size()+1,0);
    if(len==-1){
        std::cerr<<"send chat message error! "<<endl;
    }

}

// 添加好友命令 addfriend:friendid
void addfriend(int clientfd, string str){
    //获取好友id，由于在mainMenu中str已经给截取了前面的':'
    //此时剩下命令只有后面的friendid了
    int friendid=atoi(str.c_str());
    json js;
    js["msgid"]=int(EnMsgType::ADD_FRIEND_MSG);
    js["id"]=m_currentUser.getId();
    js["friendid"]=friendid;

    string buffer(js.dump());

    //发送js
    int len=send(clientfd,buffer.c_str(),buffer.size()+1,0);
    if(len==-1){
        std::cerr<<"send addfriend msg error! -> "<<buffer<<endl;
    }


}

// 创建群组命令 creategroup:groupname:groupdesc
void creategroup(int clientfd, string str){
    //按照':'拆分字符串
    int idx=str.find(':');
    if(idx==string::npos){
        //格式错误
        std::cerr<<"creategroup command invalid! "<<endl;
        return;
    }

    string groupName=str.substr(0,idx);
    string groupDesc=str.substr(idx+1,str.size()-idx);

    json js;
    js["msgid"]=int(EnMsgType::CREATE_GROUP_MSG);
    js["id"]=m_currentUser.getId();
    js["groupname"]=groupName;
    js["groupdesc"]=groupDesc;

    string buffer(js.dump());
    int len=send(clientfd,buffer.c_str(),buffer.size()+1,0);
    if(len==-1){
        std::cerr<<"send creategroup error -> "<<buffer<<endl;
    }

}

// 加入群组命令 addgroup:groupid
void addgroup(int clientfd, string str){
    //因为命令去掉冒号后只剩一个groupid了，所以这里直接转int
    int groupid=atoi(str.c_str());
    int userid=m_currentUser.getId();

    json js;
    js["msgid"]=int(EnMsgType::ADD_GROUP_MSG);
    js["id"]=userid;
    js["groupid"]=groupid;

    string buffer(js.dump());
    int len=send(clientfd,buffer.c_str(),buffer.size()+1,0);
    if(len==-1){
        std::cerr<<"addgroup send error! "<<endl;
    }

}

// 群组聊天命令 groupchat:groupid:message
void groupchat(int client, string str){
    //截取':'
    int idx=str.find(':');
    if(idx==-1){
        std::cerr<<"groupchat command error! "<<endl;
        return;
    }

    int groupid=atoi(str.substr(0,idx).c_str());
    string chatMsg=str.substr(idx+1,str.size()-idx);

    json js;
    js["msgid"]=int(EnMsgType::GROUP_CHAT_MSG);
    js["id"]=m_currentUser.getId();//说话人id
    js["name"]=m_currentUser.getName();
    js["groupid"]=groupid;
    js["msg"]=chatMsg;
    js["time"]=getCurrentTime();//说话时间

    string buffer(js.dump());
    int len=send(client,buffer.c_str(),buffer.size()+1,0);
    if(len==-1){
        std::cerr<<"groupchat send error! "<<endl;
    }

}

// 退出账号命令 loginout
void loginout(int clientfd, string str){
    int userid=m_currentUser.getId();

    json js;
    js["msgid"]=int(EnMsgType::LOGINOUT_MSG);
    js["id"]=userid;

    string buffer(js.dump());
    int len=send(clientfd,buffer.c_str(),buffer.size()+1,0);
    if(len==-1){
        std::cerr<<"loginout send error! "<<endl;
    }else{
        isMainMenuRunning=false;
    }

}

#pragma endregion   各功能函数的实现


